// Fireworks JavaScript Command // Install by copying to Fireworks/Configuration/Commands/ // Run in Fireworks via the Commands menu // Aaron Beall 2007-2011 - http://abeall.com // Version 2.1 /* TODO: - Smarter approximation which adds nodes to lesser paths to match greater paths better - Add directional preference - Add 'counter-twist' or 'offset' or similar property to adjust node mapping (to avoid unwanted twisting) - [DONE] Allow repeated blending of resulting blend objects, like a live effect - Re-blend value of '0' steps should remove the blend - Optimize: - - [DONE]predetermine blendable properties - - create local variables for object references - [DONE] Blend brush diameter - Blend brush attributes: ink, spacing, flow rate, texture, edge texture, tips, tip spacing, edge, aspect, angle, sensitivity - Blend gradients - Add interpolation option: linear (current), smooth - Add jitter option BUGS - Rounding of step count sometimes creates a single 'gap' next to original paths */ var dom = fw.selection ? fw.getDocumentDOM() : false; // document object var sel = new Array().concat(fw.selection); function BlendPaths() { // require selection if (!sel.length) return false; // validate selection var paths = [], blends = []; for(var s in sel){ if(sel[s].contours) paths.push(sel[s]); if(String(sel[s])=='[object Group]' && sel[s].customData.BlendPaths_steps) blends.push(sel[s]); } if(paths.length < 2 && blends.length < 1) { alert('This operation requires 2 or more paths, or an existing blend object.'); return false; } if(paths.length){ var conts = paths[0].contours.length; for(var p = 0; p < paths.length; p++){ if(paths[p].contours.length != conts){ alert('You must use paths with the same number of contours.'); return false; } } } if(sel.length != paths.length+blends.length){ if(!fw.yesNoDialog('Some selected elements are not paths or blends and will be excluded from this operation. Continue?')) return false; } var prevBlends = blends.length > 0 ? blends[0].customData.BlendPaths_steps : null; //if(blends.length){ //if(!confirm('NOTE: You are re-blending one or more existing blends. This action can be significantly slower than creating blends from original paths. Continue?')) //return; // This was optmized by ungrouping before re-applying a blend and then re-grouping. // The slow down will still appear however if you re-blend something that is within another group, ie a blend group within a regular group //} // user input var steps = null, error = null; do{ if(error) alert(error); steps = prompt('Steps:', (prevBlends || fw.BlendPaths_steps || 5)); error = 'Invalid input; use positive integers only.'; }while((isNaN(Number(steps)) || Number(steps) <= 0 || String(steps).indexOf('.') != -1) && steps != null); if(steps == null) return; steps = Math.ceil(Math.abs(Number(steps))); fw.BlendPaths_steps = steps; flattenBlend = fw.yesNoDialog('Flatten blended portions(orignal paths will not be flattened)?'); var finalSelection = []; // blend paths if(paths.length){ // blend var blendGroups = blendSelection(paths,steps); // group, name, and save blend object parameters for future fw.selection = paths.concat(blendGroups); dom.group('normal'); fw.selection[0].name = 'Blend: ' + paths.length + ' paths'; fw.selection[0].customData.BlendPaths_steps = steps; finalSelection.push(fw.selection[0]); } // re-blend existing blend objects if(blends.length){ for(var b = 0; b < blends.length; b++){ fw.selection = [blends[b]]; dom.selectChildren(); var origPaths = [], blendSteps = []; for(var i = 0; i < fw.selection.length; i++){ if(fw.selection[i].customData.BlendPaths_isBlendStep || fw.selection[i] != '[object Path]') blendSteps.push(fw.selection[i]); else origPaths.push(fw.selection[i]); } if(blendSteps.length){ fw.selection = blendSteps; dom.deleteSelection(false); } /*fw.selection = origPaths; blendSelection(origPaths,steps); dom.selectParents(); */ fw.selection = [blends[b]]; dom.ungroup(); var blendGroups = blendSelection(origPaths,steps); fw.selection = origPaths.concat(blendGroups); dom.group(); fw.selection[0].name = 'Blend: ' + origPaths.length + ' paths'; fw.selection[0].customData.BlendPaths_steps = steps; finalSelection.push(fw.selection[0]); } } fw.selection = finalSelection; return true; } BlendPaths(); // blend selection of elements function blendSelection(paths,steps){ // spread steps over selection var blendStep = Math.floor(steps/(paths.length-1)); var blendGroups = []; for(var i = 1 ; i < paths.length ; i++){ fw.selection = paths; blendGroups.push(blendBetween(fw.selection[i-1], fw.selection[i], blendStep + 1)); } return blendGroups; } // blend between two paths function blendBetween(elem1, elem2, steps){ var blendElements = []; fw.selection = [elem1]; dom.cloneSelection(); var clonerElem = fw.selection[0]; dom.arrange('backward'); // pre-determine properties that need to blend var blendOpacity = elem1.opacity != elem2.opacity; var blendFillColor = elem1.pathAttributes.fill && elem1.pathAttributes.fillColor != elem2.pathAttributes.fillColor; var blendBrushColor = elem1.pathAttributes.brush && elem1.pathAttributes.brushColor != elem2.pathAttributes.brushColor; var blendBrushDiameter = elem1.pathAttributes.brush && elem1.pathAttributes.brush.diameter != elem2.pathAttributes.brush.diameter; var blendFeather = elem1.pathAttributes.fill && elem1.pathAttributes.fill.feather != elem2.pathAttributes.fill.feather; // now the blending for(var i = 1 ; i < steps ; i++){ fw.selection = [clonerElem]; dom.cloneSelection(); // nodes for(var c = 0; c < elem1.contours.length; c++){ for(var n = 0; n < elem1.contours[c].nodes.length; n++){ var n1 = fw.selection[0].contours[c].nodes[n]; var ratio = (Number(n) + 1) / elem1.contours[c].nodes.length; var n2 = elem2.contours[c].nodes[Math.ceil(elem2.contours[c].nodes.length * ratio) - 1]; n1.x += ((n2.x - n1.x) / steps) * i; n1.y += ((n2.y - n1.y) / steps) * i; n1.predX += ((n2.predX - n1.predX) / steps) * i; n1.predY += ((n2.predY - n1.predY) / steps) * i; n1.succX += ((n2.succX - n1.succX) / steps) * i; n1.succY += ((n2.succY - n1.succY) / steps) * i; } } // opacity if(blendOpacity) fw.selection[0].opacity += ((elem2.opacity - elem1.opacity) / steps) * i; // fillColor if(blendFillColor){ var c1 = hexToRGB(elem1.pathAttributes.fillColor); var c2 = hexToRGB(elem2.pathAttributes.fillColor); var c = {r:c1.r,g:c1.g,b:c1.b}; c.r = Math.floor(c.r + ((c2.r - c1.r) / steps) * i); c.g = Math.floor(c.g + ((c2.g - c1.g) / steps) * i); c.b = Math.floor(c.b + ((c2.b - c1.b) / steps) * i); fw.selection[0].pathAttributes.fillColor = rgbToHex(c); } // brushColor if(blendBrushColor){ var c1 = hexToRGB(elem1.pathAttributes.brushColor); var c2 = hexToRGB(elem2.pathAttributes.brushColor); var c = {r:c1.r, g:c1.g, b:c1.b}; c.r = Math.round(c.r + ((c2.r - c1.r) / steps) * i); c.g = Math.round(c.g + ((c2.g - c1.g) / steps) * i); c.b = Math.round(c.b + ((c2.b - c1.b) / steps) * i); fw.selection[0].pathAttributes.brushColor = rgbToHex(c); } // brush.diameter if(blendBrushDiameter) fw.selection[0].pathAttributes.brush.diameter += ((elem2.pathAttributes.brush.diameter - elem1.pathAttributes.brush.diameter) / steps) * i; // feather if(blendFeather) fw.selection[0].pathAttributes.fill.feather += ((elem2.pathAttributes.fill.feather-elem1.pathAttributes.fill.feather)/steps)*i; blendElements.push(fw.selection[0]); } fw.selection = [clonerElem]; dom.deleteSelection(false); fw.selection = blendElements; dom.group('normal'); if(flattenBlend) dom.flattenSelection(); fw.selection[0].name = 'Blend: ' + blendElements.length + ' steps';//+(flattenBlend?'(flattened)':''); fw.selection[0].customData.BlendPaths_isBlendStep = true; return fw.selection[0]; } // hex to RGB function hexToRGB(hex){ // #RRGGBB var r = parseInt(hex.substr(1,2),16); var g = parseInt(hex.substr(3,2),16); var b = parseInt(hex.substr(5,2),16); return {r:r,g:g,b:b}; } // RGB to hex function rgbToHex(rgb){ var r = rgb.r.toString(16); if(r.length==1) r = '0'+r; var g = rgb.g.toString(16); if(g.length==1) g = '0'+g; var b = rgb.b.toString(16); if(b.length==1) b = '0'+b; return '#'+r+g+b; }